﻿using System;
using System.Text;
using Microsoft.Xna.Framework;


namespace GeoClipmapping
{
    public class Camera : IFormattable
    {
        #region Constants
        public const float DEF_FOV = MathHelper.PiOver4;
        public const float DEF_ASPECT = 640 / 480;
        public const float DEF_NEARPLANE = 1.0f;
        public const float DEF_FARPLANE = 1000.0f;
        public readonly Vector3 DEF_POSITION = Vector3.Backward * 10;
        public readonly Vector3 DEF_UPVECTOR = Vector3.Up;
        public readonly Vector3 DEF_RIGHTVEC = Vector3.Right;
        public readonly Vector3 DEF_TARGET = Vector3.Zero;
        #endregion

        #region Properties
        private Matrix view;
        /// <summary>
        /// Gets the current viewmatrix.
        /// </summary>
        public Matrix View
        {
            get { return view; }
        }

        private Matrix projection;
        /// <summary>
        /// Gets the current projectionmatrix.
        /// </summary>
        public Matrix Projection
        {
            get { return projection; }
        }

        private BoundingFrustum viewFrustum;
        /// <summary>
        /// Gets the frustum of the current view.
        /// </summary>
        public BoundingFrustum ViewFrustum
        {
            get { return viewFrustum; }
        }

        private Vector3 position;
        /// <summary>
        /// Gets or sets current cameraposition.
        /// </summary>
        public Vector3 Position
        {
            get { return position; }
            set { position = value; }
        }


        private Vector3 target;
        /// <summary>
        /// Gets or sets the position where the camera looks at.
        /// </summary>
        public Vector3 Target
        {
            get { return target; }
            set { target = value; }
        }

        private Vector3 upVector;
        /// <summary>
        /// Gets or sets the up vector.
        /// </summary>
        public Vector3 UpVector
        {
            get { return upVector; }
            set { upVector = value; }
        }

        private Vector3 forward;
        /// <summary>
        /// Gets the face direction of the camera.
        /// Seting a new direction will calculate new targetposition
        /// </summary>
        public Vector3 Forward
        {
            get { return forward; }
        }

        private Vector3 right;
        /// <summary>
        /// Gets the direction to the right of the camera.
        /// </summary>
        public Vector3 Right
        {
            get { return right; }
        }

        private float fieldOfView;
        /// <summary>
        /// Gets or sets the field of view value.
        /// </summary>
        public float FieldOfView
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }

        private float aspectRatio;
        /// <summary>
        /// Gets or sets the aspect ration of the camera.
        /// </summary>
        public float AspectRatio
        {
            get { return aspectRatio; }
            set { aspectRatio = value; }
        }

        private float farplane;
        /// <summary>
        /// Gets or sets the distance to farplane.
        /// </summary>
        public float Farplane
        {
            get { return farplane; }
            set { farplane = value; }
        }

        private float nearplane;
        /// <summary>
        /// Gets or sets the distance to the near plane.
        /// </summary>
        public float Nearplane
        {
            get { return nearplane; }
            set { nearplane = value; }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new Camera.
        /// </summary>
        /// <param name="device">The graphics device</param>
        public Camera()
        {
            Reset();
        }
        #endregion

        #region Methods
        /// <summary>
        /// Resets the cameravalues to ther defaultvalues.
        /// </summary>
        public void Reset()
        {
            fieldOfView = DEF_FOV;
            aspectRatio = DEF_ASPECT;
            nearplane = DEF_NEARPLANE;
            farplane = DEF_FARPLANE;
            position = DEF_POSITION;
            upVector = DEF_UPVECTOR;
            target = DEF_TARGET;
            viewFrustum = new BoundingFrustum(Matrix.Multiply(view, projection));
            Refresh();
        }

        /// <summary>
        /// Recalculates all matrices.
        /// The view matrix is calculated from postition, target and upVector, so keep
        /// them always up to date.
        /// </summary>
        public void Refresh()
        {
            forward = target - position;
            forward.Normalize();

            view = Matrix.CreateLookAt(position, target, upVector);

            projection = Matrix.CreatePerspectiveFieldOfView(
                fieldOfView, aspectRatio, nearplane, farplane);

            viewFrustum.Matrix = Matrix.Multiply(view, projection);
        }

        /// <summary>
        /// Rotates the camera using position as a fixed point. Targetposition will be changed
        /// </summary>
        /// <param name="yaw"></param>
        /// <param name="pitch"></param>
        /// <param name="roll"></param>
        public void SetRotationAroudPosition(float yaw, float pitch, float roll)
        {
            Matrix rotationMatrix = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);

            this.forward = Vector3.Transform(Vector3.Forward, rotationMatrix);
            this.right = Vector3.Transform(Vector3.Right, rotationMatrix);
            this.upVector = Vector3.Transform(Vector3.Up, rotationMatrix);

            float distance = Vector3.Distance(position, target);
            this.target = position + forward * distance;
        }

        /// <summary>
        /// Rotates the camera using viewtarget as fixed point. Position will be changed.
        /// </summary>
        /// <param name="yaw"></param>
        /// <param name="pitch"></param>
        /// <param name="roll"></param>
        public void SetRotationAroundTarget(float yaw, float pitch, float roll)
        {
            Matrix rotationMatrix = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);

            this.forward = Vector3.Transform(Vector3.Forward, rotationMatrix);
            this.right = Vector3.Transform(Vector3.Right, rotationMatrix);
            this.upVector = Vector3.Transform(Vector3.Up, rotationMatrix);

            float distance = Vector3.Distance(position, target);
            this.position = target - forward * distance;
        }

        /// <summary>
        /// Sets viewtarget to the specific distance from cameraposition.
        /// Cameraposition is fixed.
        /// </summary>
        /// <param name="distance"></param>
        public void SetDistanceFromPosition(float distance)
        {
            this.target = position + forward * distance;
        }

        /// <summary>
        /// Sets the cameraposition to the specific distance from vietarget.
        /// Viewtarget is fixed.
        /// </summary>
        /// <param name="distance"></param>
        public void SetDistanceFromTarget(float distance)
        {
            this.position = target - forward * distance;
        }

        /// <summary>
        /// Gets the stringrepresentation of this camera.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format("Camera pos:{0} tar:{1}", position.ToString(), target.ToString());
        }
        #endregion

        #region IFormattable Member
        /// <summary>
        /// supported formats:
        /// pt - position and target
        /// ptu - position, target and upvector
        /// nf - neaplane and farplane
        /// ta - average frametime
        /// tc - current frametime
        /// th - highest frametime
        /// tl - lowest frametime
        /// fps-stats - all fps statistics
        /// </summary>
        /// <param name="format"></param>
        /// <param name="formatProvider"></param>
        /// <returns></returns>
        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (formatProvider != null)
            {
                ICustomFormatter fmt = formatProvider.GetFormat(this.GetType()) as ICustomFormatter;
                if (fmt != null)
                {
                    return fmt.Format(format, this, formatProvider);
                }
            }

            StringBuilder b = new StringBuilder();
            switch (format)
            {
                case "pt":
                    b.AppendLine(String.Format("Position : {0}", position.ToString()));
                    b.AppendLine(String.Format("Target   : {0}", target.ToString()));
                    break;
                case "ptu":
                    b.AppendLine(String.Format("Position : {0}", position.ToString()));
                    b.AppendLine(String.Format("Target   : {0}", target.ToString()));
                    b.AppendLine(String.Format("Up       : {0}", upVector.ToString()));
                    break;
                case "nf":
                    b.AppendLine(String.Format("Nearplane : {0:F2}", nearplane));
                    b.AppendLine(String.Format("Farplane  : {0:F2}", farplane));
                    break;

                case "n":
                case "G":
                default:
                    return ToString();
            }
            return b.ToString();
        }
        #endregion
    }
}
